package io.hellsinger.filesystem.linux.directory

import io.hellsinger.filesystem.attribute.PathType
import io.hellsinger.filesystem.directory.Directory
import io.hellsinger.filesystem.directory.DirectoryEntry
import io.hellsinger.filesystem.directory.DirectoryWalker
import io.hellsinger.filesystem.directory.DirectoryWalker.Companion.CONTINUE
import io.hellsinger.filesystem.directory.DirectoryWalker.Companion.SKIP
import io.hellsinger.filesystem.directory.DirectoryWalker.Companion.STOP
import io.hellsinger.filesystem.linux.LinuxFileOperationProvider
import io.hellsinger.filesystem.linux.attribute.attributes
import io.hellsinger.filesystem.linux.error.ErrnoException
import io.hellsinger.filesystem.linux.path.LinuxPath
import io.hellsinger.filesystem.path.Path
import io.hellsinger.filesystem.path.PathOwner
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

interface LinuxDirectory<P : Path> : Directory<P> {
    val name: String
        get() = path.getNameAt().toString()

    suspend fun count(): Int
}

private class LinuxDirectoryImpl<P : Path>(
    override val path: P,
) : LinuxDirectory<P> {
    override fun walk(walker: DirectoryWalker): Flow<DirectoryEntry<P>> =
        flow {
            val pointer = LinuxFileOperationProvider.openDirectoryPointer(path.bytes)
            var entry = LinuxFileOperationProvider.getDirectoryEntry(pointer)
            while (entry != null) {
                /**
                 * '.' and '..' are simple sym-links to 'current' and 'previous' directories,
                 * so just skip them
                 * **/
                if (entry.path.contentEquals(".".encodeToByteArray()) || entry.path.contentEquals("..".encodeToByteArray())) {
                    entry = LinuxFileOperationProvider.getDirectoryEntry(pointer)
                    continue
                }

                val resolvedPath = path.resolve(LinuxPath(entry.path)) as P
                val type = PathType.fromDirent(entry.type)
                if (type == PathType.Directory) {
                    when (walker.onPreEnterDirectory(resolvedPath)) {
                        STOP -> {
                            walker.onPostEnterDirectory(resolvedPath)
                            break
                        }

                        SKIP -> emit(entry.wrap(resolvedPath, type))

                        CONTINUE -> {
                            emit(entry.wrap(resolvedPath, type))
                            emitAll(LinuxDirectoryImpl(resolvedPath).walk(walker))
                        }
                    }
                    walker.onPostEnterDirectory(resolvedPath)
                } else {
                    when (walker.onEntry(resolvedPath)) {
                        STOP -> {
                            emit(entry.wrap(resolvedPath, type))
                            break
                        }

                        // Do nothing
                        SKIP -> {}

                        else -> emit(entry.wrap(resolvedPath, type))
                    }
                }

                entry = LinuxFileOperationProvider.getDirectoryEntry(pointer)
            }

            LinuxFileOperationProvider.closeDirectoryPointer(pointer)
        }

    override suspend fun count(): Int = LinuxFileOperationProvider.getDirectoryCount(path.bytes)
}

fun <P : Path> P.walk(walker: DirectoryWalker): Flow<DirectoryEntry<P>> =
    if (attributes.type == PathType.Directory) asLinuxDirectory().walk(walker) else emptyFlow()

fun <P : Path> P.asLinuxDirectory(): LinuxDirectory<P> = LinuxDirectoryImpl(this)

fun <P : Path> PathOwner<P>.asLinuxDirectory(): LinuxDirectory<P> = LinuxDirectoryImpl(path)

internal class LinuxDirectoryEntryImpl(
    val path: ByteArray,
    val id: Long,
    val type: Int,
) {
    fun <P : Path> wrap(
        resolvedPath: P,
        resolvedType: PathType,
    ): LinuxDirectoryEntry<P> =
        object : LinuxDirectoryEntry<P> {
            override val id: Long = this@LinuxDirectoryEntryImpl.id
            override val type: PathType = resolvedType
            override val path: P = resolvedPath
        }
}

fun <P : Path> LinuxDirectory<P>.defaultWalker(): Flow<DirectoryEntry<P>> =
    walk(
        object : DirectoryWalker {
            override suspend fun onPreEnterDirectory(path: Path): Int = SKIP

            override suspend fun onPostEnterDirectory(path: Path) {}

            override suspend fun onEntry(path: Path): Int = CONTINUE
        },
    )

fun <P : Path> LinuxDirectory<P>.treeWalker(): Flow<DirectoryEntry<P>> =
    walk(
        object : DirectoryWalker {
            override suspend fun onPreEnterDirectory(path: Path): Int = CONTINUE

            override suspend fun onPostEnterDirectory(path: Path) {}

            override suspend fun onEntry(path: Path): Int = CONTINUE
        },
    )

suspend fun <P : Path> Directory<P>.size(): Long =
    suspendCoroutine { continuation ->
        try {
            continuation.resume(LinuxFileOperationProvider.getDirectorySize(path.bytes))
        } catch (exception: ErrnoException) {
            continuation.resumeWithException(exception) // rethrow
        }
    }
